﻿namespace Jsl.FxCop
{
    using System.Collections.Generic;
    using System.Linq;
    using Microsoft.FxCop.Sdk;

    /// <summary>
    /// Finds reference parameters that are never set.
    /// </summary>
    /// <remarks>
    /// Parameters that are passed by reference should be set somewhere within the method. If they are never set then they
    /// should be passed by value.
    /// </remarks>
    public class ReferenceParametersShouldHaveTheirValuesSet : BaseIntrospectionRule
    {
        #region Constructors
        /// <summary>
        /// Initializes a new instance of the <see cref="ReferenceParametersShouldHaveTheirValuesSet"/> class.
        /// </summary>
        public ReferenceParametersShouldHaveTheirValuesSet()
            : base(typeof(ReferenceParametersShouldHaveTheirValuesSet).Name, "Jsl.FxCop.Rules", typeof(ReferenceParametersShouldHaveTheirValuesSet).Assembly)
        {
        }
        #endregion Constructors

        #region Public Methods
        /// <summary>
        /// Checks the specified member.
        /// </summary>
        /// <param name="member">The member.</param>
        /// <returns>The problems found, if any.</returns>
        public override ProblemCollection Check(Member member)
        {
            // If this is a method then visit it.
            (member as Method)
                .DoIfNotNull(this.ProcessMethod);

            return this.Problems;
        }
        #endregion Public Methods

        #region Private Methods
        private void CheckAssignmentStatement(AssignmentStatement assignment, IList<Parameter> referenceParameters)
        {
            (assignment.Target as AddressDereference)
                .IfNotNull(address => address.Address as Parameter)
                .DoIfNotNull(parameter => referenceParameters.Remove(parameter));
        }

        private void CheckMethodCall(MethodCall call, IList<Parameter> referenceParameters)
        {
            // Remove any parameters that are passed directly as operands.
            var parameterOperands = call.Operands.OfType<Parameter>();
            foreach (var parameter in parameterOperands)
            {
                referenceParameters.Remove(parameter);
            }
        }

        private void ProcessMethod(Method method)
        {
            // If this is an interface method or an abstract method then exit since there is no code to check.
            if ((method.Body.Statements.Count == 0) || method.DeclaringType.IsDelegate())
            {
                return;
            }

            // If this is an implementation of an interface method then ignore it.
            if (method.GetInterfaceMethodImplemented() != null)
            {
                return;
            }

            var referenceParameters =
                (from parameter in method.Parameters
                 where !parameter.IsOut
                    && (parameter.Type is Reference)
                    && !((Reference)parameter.Type).ElementType.IsValueType
                 select parameter)
                .ToList();

            if (referenceParameters.Any())
            {
                // Remove any parameters that were used.
                new CciVisitor()
                    .AddVisitor<AssignmentStatement>(assignment => this.CheckAssignmentStatement(assignment, referenceParameters))
                    .AddVisitor<MethodCall>(call => this.CheckMethodCall(call, referenceParameters))
                    .Visit(method);

                // Add a problem for any that are left.
                foreach (var referenceParameter in referenceParameters)
                {
                    this.Problems.Add(
                        new Problem(
                            this.GetResolution(referenceParameter.Name.Name),
                            method));
                }
            }
        }
        #endregion Private Methods
    }
}